Laboratorio 9: Optimización de modelos 💯

MDS7202: Laboratorio de Programación Científica para Ciencia de Datos

Cuerpo Docente:¶

  • Profesor: Ignacio Meza, Gabriel Iturra
  • Auxiliar: Sebastián Tinoco
  • Ayudante: Arturo Lazcano, Angelo Muñoz

Equipo: SUPER IMPORTANTE - notebooks sin nombre no serán revisados¶

  • Nombre de alumno 1: Daniel Minaya
  • Nombre de alumno 2:

Temas a tratar¶

  • Predicción de demanda usando xgboost
  • Búsqueda del modelo óptimo de clasificación usando optuna
  • Uso de pipelines.

Reglas:¶

  • Grupos de 2 personas
  • Cualquier duda fuera del horario de clases al foro. Mensajes al equipo docente serán respondidos por este medio.
  • Prohibidas las copias.
  • Pueden usar cualquer material del curso que estimen conveniente.

Objetivos principales del laboratorio¶

  • Optimizar modelos usando optuna
  • Recurrir a técnicas de prunning
  • Forzar el aprendizaje de relaciones entre variables mediante constraints
  • Fijar un pipeline con un modelo base que luego se irá optimizando.

El laboratorio deberá ser desarrollado sin el uso indiscriminado de iteradores nativos de python (aka "for", "while"). La idea es que aprendan a exprimir al máximo las funciones optimizadas que nos entrega pandas, las cuales vale mencionar, son bastante más eficientes que los iteradores nativos sobre DataFrames.

Link de repositorio de GitHub: https://github.com/DanielMinaya1/MDS7202¶

Importamos librerias útiles¶

In [1]:
!pip install -qq xgboost optuna

1. El emprendimiento de Fiu¶

Tras liderar de manera exitosa la implementación de un proyecto de ciencia de datos para caracterizar los datos generados en Santiago 2023, el misterioso corpóreo Fiu se anima y decide levantar su propio negocio de consultoría en machine learning. Tras varias e intensas negociaciones, Fiu logra encontrar su primera chamba: predecir la demanda (cantidad de venta) de una famosa productora de bebidas de calibre mundial. Como usted tuvo un rendimiento sobresaliente en el proyecto de caracterización de datos, Fiu lo contrata como data scientist de su emprendimiento.

Para este laboratorio deben trabajar con los datos sales.csv subidos a u-cursos, el cual contiene una muestra de ventas de la empresa para diferentes productos en un determinado tiempo.

Para comenzar, cargue el dataset señalado y visualice a través de un .head los atributos que posee el dataset.

Fiu siendo felicitado por su excelente desempeño en el proyecto de caracterización de datos

In [2]:
import pandas as pd
import numpy as np
from datetime import datetime

df = pd.read_csv('sales.csv')
df['date'] = pd.to_datetime(df['date'], format='%d/%m/%y')

1.1 Generando un Baseline (0.5 puntos)¶

Antes de entrenar un algoritmo, usted recuerda los apuntes de su magíster en ciencia de datos y recuerda que debe seguir una serie de buenas prácticas para entrenar correcta y debidamente su modelo. Después de un par de vueltas, llega a las siguientes tareas:

  1. Separe los datos en conjuntos de train (70%), validation (20%) y test (10%). Fije una semilla para controlar la aleatoriedad.
  2. Implemente un FunctionTransformer para extraer el día, mes y año de la variable date. Guarde estas variables en el formato categorical de pandas.
  3. Implemente un ColumnTransformer para procesar de manera adecuada los datos numéricos y categóricos. Use OneHotEncoder para las variables categóricas.
  4. Guarde los pasos anteriores en un Pipeline, dejando como último paso el regresor DummyRegressor para generar predicciones en base a promedios.
  5. Entrene el pipeline anterior y reporte la métrica mean_absolute_error sobre los datos de validación. ¿Cómo se interpreta esta métrica para el contexto del negocio?
  6. Finalmente, vuelva a entrenar el Pipeline pero esta vez usando XGBRegressor como modelo utilizando los parámetros por default. ¿Cómo cambia el MAE al implementar este algoritmo? ¿Es mejor o peor que el DummyRegressor?
  7. Guarde ambos modelos en un archivo .pkl (uno cada uno)

Librerías¶

In [3]:
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import FunctionTransformer, OneHotEncoder, MinMaxScaler
from sklearn.compose import ColumnTransformer, TransformedTargetRegressor
from sklearn.pipeline import Pipeline
from sklearn.dummy import DummyRegressor
from sklearn.metrics import mean_absolute_error
from xgboost import XGBRegressor
import pickle
from sklearn.base import BaseEstimator, TransformerMixin
import optuna
from optuna.samplers import TPESampler
import time
from optuna.integration import XGBoostPruningCallback
from optuna.visualization import plot_optimization_history
from optuna.visualization import plot_parallel_coordinate
from optuna.visualization import plot_param_importances

# Semilla
seed = 42

1.1.0 Análisis exploratorio¶

In [4]:
df.head()
Out[4]:
id date city lat long pop shop brand container capacity price quantity
0 0 2012-01-31 Athens 37.97945 23.71622 672130 shop_1 kinder-cola glass 500ml 0.96 13280
1 1 2012-01-31 Athens 37.97945 23.71622 672130 shop_1 kinder-cola plastic 1.5lt 2.86 6727
2 2 2012-01-31 Athens 37.97945 23.71622 672130 shop_1 kinder-cola can 330ml 0.87 9848
3 3 2012-01-31 Athens 37.97945 23.71622 672130 shop_1 adult-cola glass 500ml 1.00 20050
4 4 2012-01-31 Athens 37.97945 23.71622 672130 shop_1 adult-cola can 330ml 0.39 25696
In [5]:
df['date'].min(), df['date'].max()
Out[5]:
(Timestamp('2012-01-31 00:00:00'), Timestamp('2018-12-31 00:00:00'))
In [6]:
df['city'].value_counts()
Out[6]:
city
Athens          2482
Thessaloniki    1246
Patra           1245
Larisa          1242
Irakleion       1241
Name: count, dtype: int64
In [7]:
df['lat'].value_counts(), df['long'].value_counts()
Out[7]:
(lat
 40.64361    1246
 38.24444    1245
 37.96245    1242
 37.97945    1241
 35.32787    1241
 39.63689    1241
 Name: count, dtype: int64,
 long
 21.73444    1246
 22.93086    1246
 22.41761    1242
 25.14341    1241
 23.68708    1241
 23.71622    1240
 Name: count, dtype: int64)
In [8]:
df['pop'].unique()
Out[8]:
array([672130, 134219, 164250, 346502, 137540, 671022, 135432, 166301,
       347001, 139242, 668203, 136202, 167242, 349232, 140563, 667237,
       138560, 167001, 351650, 141732, 665102, 137302, 168254, 351702,
       142030, 665871, 138200, 168501, 353001, 144302, 664046, 137154,
       168034, 354290, 144651], dtype=int64)
In [9]:
df['shop'].value_counts()
Out[9]:
shop
shop_4    1246
shop_6    1245
shop_5    1242
shop_1    1241
shop_2    1241
shop_3    1241
Name: count, dtype: int64
In [10]:
df['brand'].value_counts()
Out[10]:
brand
kinder-cola     1495
adult-cola      1493
lemon-boost     1493
gazoza          1491
orange-power    1484
Name: count, dtype: int64
In [11]:
df['container'].value_counts()
Out[11]:
container
glass      2486
plastic    2485
can        2485
Name: count, dtype: int64
In [12]:
df['capacity'].value_counts()
Out[12]:
capacity
330ml    2486
500ml    2485
1.5lt    2485
Name: count, dtype: int64
In [13]:
df['price'].value_counts()
Out[13]:
price
0.59    96
0.65    88
0.56    86
0.66    81
0.71    78
        ..
4.33     1
4.31     1
4.42     1
0.11     1
4.53     1
Name: count, Length: 402, dtype: int64
In [14]:
df['quantity'].value_counts()
Out[14]:
quantity
22207    4
24057    3
31205    3
25620    3
15019    3
        ..
15475    1
62328    1
25512    1
17555    1
24615    1
Name: count, Length: 6906, dtype: int64

Suponemos que el dataset contiene las ventas de bebidas por tiendas. Las columnas serían:

  • id: identificador de la fila,
  • date: fecha de compra, desde 2012 hasta 2018
  • city: ciudad de la tienda, se encuentran en Grecia,
  • lat, long: latitud y longitud de la tienda,
  • pop: población de la ciudad (varía con la fecha),
  • shop: tienda,
  • brand: marca de la bebida,
  • container: contenedor de la bebida,
  • price: precio de la bebida
  • quantity: cantidad comprada
In [15]:
df['id'].hist()
plt.show()
In [16]:
df.drop_duplicates(subset=['shop', 'lat', 'long'])
Out[16]:
id date city lat long pop shop brand container capacity price quantity
0 0 2012-01-31 Athens 37.97945 23.71622 672130 shop_1 kinder-cola glass 500ml 0.96 13280
11 11 2012-01-31 Irakleion 35.32787 25.14341 134219 shop_2 kinder-cola glass 500ml 1.51 8943
22 23 2012-01-31 Patra 38.24444 21.73444 164250 shop_6 kinder-cola glass 500ml 1.11 17474
35 36 2012-01-31 Thessaloniki 40.64361 22.93086 346502 shop_4 kinder-cola plastic 1.5lt 2.80 11306
45 46 2012-01-31 Athens 37.96245 23.68708 672130 shop_3 kinder-cola plastic 1.5lt 3.21 5630
56 57 2012-01-31 Larisa 39.63689 22.41761 137540 shop_5 kinder-cola glass 500ml 1.38 7495
6736 6840 2018-05-31 Athens 37.97945 21.73444 664046 shop_1 kinder-cola plastic 1.5lt 3.30 13753
6982 7086 2018-07-31 Larisa 37.96245 22.41761 144651 shop_5 kinder-cola glass 500ml 1.28 20185
In [17]:
df.drop_duplicates(subset=['shop', 'lat'])
Out[17]:
id date city lat long pop shop brand container capacity price quantity
0 0 2012-01-31 Athens 37.97945 23.71622 672130 shop_1 kinder-cola glass 500ml 0.96 13280
11 11 2012-01-31 Irakleion 35.32787 25.14341 134219 shop_2 kinder-cola glass 500ml 1.51 8943
22 23 2012-01-31 Patra 38.24444 21.73444 164250 shop_6 kinder-cola glass 500ml 1.11 17474
35 36 2012-01-31 Thessaloniki 40.64361 22.93086 346502 shop_4 kinder-cola plastic 1.5lt 2.80 11306
45 46 2012-01-31 Athens 37.96245 23.68708 672130 shop_3 kinder-cola plastic 1.5lt 3.21 5630
56 57 2012-01-31 Larisa 39.63689 22.41761 137540 shop_5 kinder-cola glass 500ml 1.38 7495
6982 7086 2018-07-31 Larisa 37.96245 22.41761 144651 shop_5 kinder-cola glass 500ml 1.28 20185
In [18]:
df.drop_duplicates(subset=['shop', 'long'])
Out[18]:
id date city lat long pop shop brand container capacity price quantity
0 0 2012-01-31 Athens 37.97945 23.71622 672130 shop_1 kinder-cola glass 500ml 0.96 13280
11 11 2012-01-31 Irakleion 35.32787 25.14341 134219 shop_2 kinder-cola glass 500ml 1.51 8943
22 23 2012-01-31 Patra 38.24444 21.73444 164250 shop_6 kinder-cola glass 500ml 1.11 17474
35 36 2012-01-31 Thessaloniki 40.64361 22.93086 346502 shop_4 kinder-cola plastic 1.5lt 2.80 11306
45 46 2012-01-31 Athens 37.96245 23.68708 672130 shop_3 kinder-cola plastic 1.5lt 3.21 5630
56 57 2012-01-31 Larisa 39.63689 22.41761 137540 shop_5 kinder-cola glass 500ml 1.38 7495
6736 6840 2018-05-31 Athens 37.97945 21.73444 664046 shop_1 kinder-cola plastic 1.5lt 3.30 13753

Podemos ver que la columna id sigue casi una distribución uniforme, por lo que podemos prescindir de esta columna para los modelos. Además, las columnas lat y long si bien son numéricas, podemos tratarlas como variables categóricas debido a la baja cantidad de categorías únicas y además cada categoría creada tendría casi la misma cantidad de datos. Sin embargo, como (lat, long) corresponde a la posición de la tienda, entonces hay una alta correlación entre estas tres variables, por lo que deberíamos evaluar el desempeño de los modelos al incluir estas variables.

Con respecto al tipo de variables, consideraremos solo pop, price y quantity como variables numéricas, y como pop y price representan valores positivos utilizaremos MinMaxScaler para escalarlos, mientras que price no será escalado ya que es nuestra variable objetivo.

1.1.1 Separación de datos¶

In [19]:
# Separamos features y labels
X, y = df.drop(columns=['quantity']), df['quantity']

# Separamos en train (70%), validation (20%) y split (10%)
X_train, X_valtest, y_train, y_valtest = train_test_split(X, y, 
                                                          test_size=0.3, 
                                                          random_state=seed, 
                                                          shuffle=True,
                                                         )
X_val, X_test, y_val, y_test = train_test_split(X_valtest, y_valtest, 
                                                test_size=0.33, 
                                                random_state=seed,
                                                shuffle=True,
                                               )

print("Train:", len(X_train))
print("Val:", len(X_val))
print("Test:", len(X_test))
Train: 5219
Val: 1498
Test: 739

1.1.2 FunctionTransformer para la columna Date¶

In [20]:
# Definimos función para extraer día, mes y año como categorías
def extract_date_info(df):
    df['day'] = df['date'].dt.day.astype('category')
    df['month'] = df['date'].dt.month.astype('category')
    df['year'] = df['date'].dt.year.astype('category')
    
    return df.drop(columns=['date'])

# Definimos el transformer para separar date
date_transformer = FunctionTransformer(extract_date_info)

# Ejemplo
date_transformer.fit_transform(X_train)
Out[20]:
id city lat long pop shop brand container capacity price day month year
292 300 Patra 38.24444 21.73444 164250 shop_6 adult-cola plastic 1.5lt 2.54 30 4 2012
3366 3416 Athens 37.97945 23.71622 667237 shop_1 gazoza plastic 1.5lt 0.71 28 2 2015
3685 3741 Athens 37.96245 23.68708 667237 shop_3 adult-cola can 330ml 0.66 30 6 2015
2404 2441 Athens 37.97945 23.71622 668203 shop_1 gazoza can 330ml 0.30 30 4 2014
2855 2898 Irakleion 35.32787 25.14341 136202 shop_2 orange-power can 330ml 0.56 30 9 2014
... ... ... ... ... ... ... ... ... ... ... ... ... ...
5191 5275 Athens 37.96245 23.68708 665102 shop_3 adult-cola plastic 1.5lt 3.59 30 11 2016
5226 5311 Athens 37.97945 23.71622 665102 shop_1 kinder-cola plastic 1.5lt 2.78 31 12 2016
5390 5478 Athens 37.97945 23.71622 665871 shop_1 gazoza can 330ml 0.36 31 1 2017
860 873 Athens 37.96245 23.68708 672130 shop_3 gazoza glass 500ml 0.51 31 10 2012
7270 7374 Athens 37.96245 23.68708 664046 shop_3 adult-cola can 330ml 0.81 31 10 2018

5219 rows × 13 columns

1.1.3 ColumnTransformer para el dataset¶

In [21]:
# Definimos las columnas numéricas
numerical_features = [
    'pop',
    'price',
]

# Definimos las columnas categóricas
categorical_features = [
    'city',
    'lat',
    'long',
    'shop',
    'brand',
    'container',
    'capacity',
    'day', 
    'month', 
    'year',
] 

# Definimos el preprocesador para las columnas
preprocessor = ColumnTransformer(
    transformers = [
        ('num', MinMaxScaler(), numerical_features),
        ('cat', OneHotEncoder(sparse_output=False), categorical_features)
    ],
    # Eliminamos la columna id
    remainder = 'drop'
)

1.1.4 Generación de Pipelines¶

In [22]:
pipeline_dummy = Pipeline([
    ('date_transformer', date_transformer),
    ('preprocessor', preprocessor),
    ('regressor', DummyRegressor())
])

pipeline_xgb = Pipeline([
    ('date_transformer', date_transformer),
    ('preprocessor', preprocessor),
    ('regressor', XGBRegressor())
])

1.1.5 Entrenamiento¶

In [23]:
pipeline_dummy.fit(X_train, y_train)
Out[23]:
Pipeline(steps=[('date_transformer',
                 FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)),
                ('preprocessor',
                 ColumnTransformer(transformers=[('num', MinMaxScaler(),
                                                  ['pop', 'price']),
                                                 ('cat',
                                                  OneHotEncoder(sparse_output=False),
                                                  ['city', 'lat', 'long',
                                                   'shop', 'brand', 'container',
                                                   'capacity', 'day', 'month',
                                                   'year'])])),
                ('regressor', DummyRegressor())])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('date_transformer',
                 FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)),
                ('preprocessor',
                 ColumnTransformer(transformers=[('num', MinMaxScaler(),
                                                  ['pop', 'price']),
                                                 ('cat',
                                                  OneHotEncoder(sparse_output=False),
                                                  ['city', 'lat', 'long',
                                                   'shop', 'brand', 'container',
                                                   'capacity', 'day', 'month',
                                                   'year'])])),
                ('regressor', DummyRegressor())])
FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)
ColumnTransformer(transformers=[('num', MinMaxScaler(), ['pop', 'price']),
                                ('cat', OneHotEncoder(sparse_output=False),
                                 ['city', 'lat', 'long', 'shop', 'brand',
                                  'container', 'capacity', 'day', 'month',
                                  'year'])])
['pop', 'price']
MinMaxScaler()
['city', 'lat', 'long', 'shop', 'brand', 'container', 'capacity', 'day', 'month', 'year']
OneHotEncoder(sparse_output=False)
DummyRegressor()
In [24]:
pipeline_xgb.fit(X_train, y_train)
Out[24]:
Pipeline(steps=[('date_transformer',
                 FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)),
                ('preprocessor',
                 ColumnTransformer(transformers=[('num', MinMaxScaler(),
                                                  ['pop', 'price']),
                                                 ('cat',
                                                  OneHotEncoder(sparse_output=False),
                                                  ['city', 'lat', 'long',
                                                   'shop', 'brand', 'container',
                                                   'capacity', 'day', 'month',
                                                   'year'])])),
                ('regressor',
                 XGBRegressor...
                              feature_types=None, gamma=None, grow_policy=None,
                              importance_type=None,
                              interaction_constraints=None, learning_rate=None,
                              max_bin=None, max_cat_threshold=None,
                              max_cat_to_onehot=None, max_delta_step=None,
                              max_depth=None, max_leaves=None,
                              min_child_weight=None, missing=nan,
                              monotone_constraints=None, multi_strategy=None,
                              n_estimators=None, n_jobs=None,
                              num_parallel_tree=None, random_state=None, ...))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('date_transformer',
                 FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)),
                ('preprocessor',
                 ColumnTransformer(transformers=[('num', MinMaxScaler(),
                                                  ['pop', 'price']),
                                                 ('cat',
                                                  OneHotEncoder(sparse_output=False),
                                                  ['city', 'lat', 'long',
                                                   'shop', 'brand', 'container',
                                                   'capacity', 'day', 'month',
                                                   'year'])])),
                ('regressor',
                 XGBRegressor...
                              feature_types=None, gamma=None, grow_policy=None,
                              importance_type=None,
                              interaction_constraints=None, learning_rate=None,
                              max_bin=None, max_cat_threshold=None,
                              max_cat_to_onehot=None, max_delta_step=None,
                              max_depth=None, max_leaves=None,
                              min_child_weight=None, missing=nan,
                              monotone_constraints=None, multi_strategy=None,
                              n_estimators=None, n_jobs=None,
                              num_parallel_tree=None, random_state=None, ...))])
FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)
ColumnTransformer(transformers=[('num', MinMaxScaler(), ['pop', 'price']),
                                ('cat', OneHotEncoder(sparse_output=False),
                                 ['city', 'lat', 'long', 'shop', 'brand',
                                  'container', 'capacity', 'day', 'month',
                                  'year'])])
['pop', 'price']
MinMaxScaler()
['city', 'lat', 'long', 'shop', 'brand', 'container', 'capacity', 'day', 'month', 'year']
OneHotEncoder(sparse_output=False)
XGBRegressor(base_score=None, booster=None, callbacks=None,
             colsample_bylevel=None, colsample_bynode=None,
             colsample_bytree=None, device=None, early_stopping_rounds=None,
             enable_categorical=False, eval_metric=None, feature_types=None,
             gamma=None, grow_policy=None, importance_type=None,
             interaction_constraints=None, learning_rate=None, max_bin=None,
             max_cat_threshold=None, max_cat_to_onehot=None,
             max_delta_step=None, max_depth=None, max_leaves=None,
             min_child_weight=None, missing=nan, monotone_constraints=None,
             multi_strategy=None, n_estimators=None, n_jobs=None,
             num_parallel_tree=None, random_state=None, ...)
In [25]:
# Predecimos
y_pred_dummy = pipeline_dummy.predict(X_val)
y_pred_xgb = pipeline_xgb.predict(X_val)

# Calculamos MAE en conjunto de validación
mae_dummy = mean_absolute_error(y_val, y_pred_dummy)
mae_xgb = mean_absolute_error(y_val, y_pred_xgb)

# Printeamos
print(f'MAE con Dummy Regressor: {mae_dummy}')
print(f'MAE con XGB Regressor: {mae_xgb}')
MAE con Dummy Regressor: 13308.134750658153
MAE con XGB Regressor: 2441.8524880504738

1.1.6 Análisis de resultados¶

In [26]:
df['quantity'].hist()
plt.show()

Para el DummyRegressor obtuvimos un MAE de 13,000, y como la variable objetivo quantity está en ese orden de magnitud, este error es demasiado grande, por lo que este modelo no es un buen regresor. Por otro lado, con el XGBRegressor obtuvimos un MAE de 2,400, que es un orden de magnitud menos que el obtenido en el modelo anterior, por lo cual vemos que XGBRegressor se desempeña mejor que DummyRegressor lo cual era de esperarse.

Para interpretar la métrica Mean Absolute Error (MAE) lo que estamos haciendo es ver qué tan alejado están los puntos de la función de regresión, y luego promediamos esa distancia. La principal diferencia con la clásica métrica Mean Squared Error (MSE) es que está métrica penaliza todas las distancias por igual, mientras que MSE penaliza más los errores grandes, mientras que los errores pequeños son más despreciables.

1.1.7 Guardado de modelos¶

In [27]:
pickle.dump(pipeline_dummy, open('models/dummy_regressor_model.pkl', 'wb'))
pickle.dump(pipeline_xgb, open('models/dummy_regressor_model.pkl', 'wb'))

1.2 Forzando relaciones entre parámetros con XGBoost (1.0 puntos)¶

Un colega aficionado a la economía le sopla que la demanda guarda una relación inversa con el precio del producto. Motivado para impresionar al querido corpóreo, se propone hacer uso de esta información para mejorar su modelo.

Vuelva a entrenar el Pipeline, pero esta vez forzando una relación monótona negativa entre el precio y la cantidad. Luego, vuelva a reportar el MAE sobre el conjunto de validación. ¿Cómo cambia el error al incluir esta relación? ¿Tenía razón su amigo?

Nuevamente, guarde su modelo en un archivo .pkl

Nota: Para realizar esta parte, debe apoyarse en la siguiente documentación.

Hint: Para implementar el constraint, se le sugiere hacerlo especificando el nombre de la variable. De ser así, probablemente le sea útil mantener el formato de pandas antes del step de entrenamiento.

1.2.1 FunctionTransformer para pasar a DataFrame¶

In [28]:
# Creamos una función que convierta el array en DataFrame
def to_dataframe(X, preprocessor, numerical_features, categorical_features):
    num_col_names = list(preprocessor.named_transformers_['num'].get_feature_names_out(numerical_features))
    cat_col_names = list(preprocessor.named_transformers_['cat'].get_feature_names_out(categorical_features))
    col_names = num_col_names + cat_col_names
    return pd.DataFrame(X, columns=col_names)

# Obtenemos el nombre de las columnas luego de preprocesar
num_col_names = list(preprocessor.named_transformers_['num'].get_feature_names_out(numerical_features))
cat_col_names = list(preprocessor.named_transformers_['cat'].get_feature_names_out(categorical_features))
col_names = num_col_names + cat_col_names

# Definimos el transformer
to_dataframe_transformer = FunctionTransformer(to_dataframe, kw_args={'preprocessor': preprocessor,
                                                                      'numerical_features': numerical_features,
                                                                      'categorical_features': categorical_features,
                                                                     })

1.2.2 Actualización de Pipeline¶

In [29]:
# Agregamos to_dataframe a la pipeline y agregamos la restricción de monotonía
pipeline_xgb_monotonic = Pipeline([
    ('date_transformer', date_transformer),
    ('preprocessor', preprocessor),
    ('to_dataframe', to_dataframe_transformer),
    ('regressor', XGBRegressor(monotone_constraints={"price": -1})),
])

1.2.3 Entrenamiento¶

In [30]:
pipeline_xgb_monotonic.fit(X_train, y_train)
Out[30]:
Pipeline(steps=[('date_transformer',
                 FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)),
                ('preprocessor',
                 ColumnTransformer(transformers=[('num', MinMaxScaler(),
                                                  ['pop', 'price']),
                                                 ('cat',
                                                  OneHotEncoder(sparse_output=False),
                                                  ['city', 'lat', 'long',
                                                   'shop', 'brand', 'container',
                                                   'capacity', 'day', 'month',
                                                   'year'])])),
                ('to_dataframe',
                 FunctionT...
                              feature_types=None, gamma=None, grow_policy=None,
                              importance_type=None,
                              interaction_constraints=None, learning_rate=None,
                              max_bin=None, max_cat_threshold=None,
                              max_cat_to_onehot=None, max_delta_step=None,
                              max_depth=None, max_leaves=None,
                              min_child_weight=None, missing=nan,
                              monotone_constraints={'price': -1},
                              multi_strategy=None, n_estimators=None,
                              n_jobs=None, num_parallel_tree=None,
                              random_state=None, ...))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('date_transformer',
                 FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)),
                ('preprocessor',
                 ColumnTransformer(transformers=[('num', MinMaxScaler(),
                                                  ['pop', 'price']),
                                                 ('cat',
                                                  OneHotEncoder(sparse_output=False),
                                                  ['city', 'lat', 'long',
                                                   'shop', 'brand', 'container',
                                                   'capacity', 'day', 'month',
                                                   'year'])])),
                ('to_dataframe',
                 FunctionT...
                              feature_types=None, gamma=None, grow_policy=None,
                              importance_type=None,
                              interaction_constraints=None, learning_rate=None,
                              max_bin=None, max_cat_threshold=None,
                              max_cat_to_onehot=None, max_delta_step=None,
                              max_depth=None, max_leaves=None,
                              min_child_weight=None, missing=nan,
                              monotone_constraints={'price': -1},
                              multi_strategy=None, n_estimators=None,
                              n_jobs=None, num_parallel_tree=None,
                              random_state=None, ...))])
FunctionTransformer(func=<function extract_date_info at 0x000002A18D8E1000>)
ColumnTransformer(transformers=[('num', MinMaxScaler(), ['pop', 'price']),
                                ('cat', OneHotEncoder(sparse_output=False),
                                 ['city', 'lat', 'long', 'shop', 'brand',
                                  'container', 'capacity', 'day', 'month',
                                  'year'])])
['pop', 'price']
MinMaxScaler()
['city', 'lat', 'long', 'shop', 'brand', 'container', 'capacity', 'day', 'month', 'year']
OneHotEncoder(sparse_output=False)
FunctionTransformer(func=<function to_dataframe at 0x000002A1849EDE10>,
                    kw_args={'categorical_features': ['city', 'lat', 'long',
                                                      'shop', 'brand',
                                                      'container', 'capacity',
                                                      'day', 'month', 'year'],
                             'numerical_features': ['pop', 'price'],
                             'preprocessor': ColumnTransformer(transformers=[('num',
                                                                              MinMaxScaler(),
                                                                              ['pop',
                                                                               'price']),
                                                                             ('cat',
                                                                              OneHotEncoder(sparse_output=False),
                                                                              ['city',
                                                                               'lat',
                                                                               'long',
                                                                               'shop',
                                                                               'brand',
                                                                               'container',
                                                                               'capacity',
                                                                               'day',
                                                                               'month',
                                                                               'year'])])})
XGBRegressor(base_score=None, booster=None, callbacks=None,
             colsample_bylevel=None, colsample_bynode=None,
             colsample_bytree=None, device=None, early_stopping_rounds=None,
             enable_categorical=False, eval_metric=None, feature_types=None,
             gamma=None, grow_policy=None, importance_type=None,
             interaction_constraints=None, learning_rate=None, max_bin=None,
             max_cat_threshold=None, max_cat_to_onehot=None,
             max_delta_step=None, max_depth=None, max_leaves=None,
             min_child_weight=None, missing=nan,
             monotone_constraints={'price': -1}, multi_strategy=None,
             n_estimators=None, n_jobs=None, num_parallel_tree=None,
             random_state=None, ...)
In [31]:
# Predecimos
y_pred_xgb_monotonic = pipeline_xgb_monotonic.predict(X_val)

# Calculamos MAE en conjunto de validación
mae_xgb_monotonic = mean_absolute_error(y_val, y_pred_xgb_monotonic)

# Printeamos 
print(f'MAE con XGB Regressor: {mae_xgb}')
print(f'MAE con XGB Regressor + Monotonic Constraint: {mae_xgb_monotonic}')
MAE con XGB Regressor: 2441.8524880504738
MAE con XGB Regressor + Monotonic Constraint: 2477.7784169073575

1.2.4 Análisis de resultados¶

Podemos ver que el MAE obtenido por el modelo después de agregar la restricción de monotonía no afectó el desempeño del modelo, de hecho, el error aumentó ligeramente, es decir, para este conjunto de datos en particular, no se aprecia una mejora significativa, pero como tampoco se empeora el modelo, entonces podemos decir que esta restricción realmente no cumplió con su objetivo para este dataset, lo cual no quiere decir que la restricción no se pueda cumplir para otro conjunto de datos, es decir, no podemos asegurar que nuestro amigo tuvo razón, pero tampoco podemos decir que esté equivocado.

1.2.5 Guardado del modelo¶

In [32]:
pickle.dump(pipeline_xgb_monotonic, open('models/xgb_regressor_monotonic_model.pkl', 'wb'))

1.3 Optimización de Hiperparámetros con Optuna (2.0 puntos)¶

Luego de presentarle sus resultados, Fiu le pregunta si es posible mejorar aun más su modelo. En particular, le comenta de la optimización de hiperparámetros con metodologías bayesianas a través del paquete optuna. Como usted es un aficionado al entrenamiento de modelos de ML, se propone implementar la descabellada idea de su jefe.

A partir de la mejor configuración obtenida en la sección anterior, utilice optuna para optimizar sus hiperparámetros. En particular, se le pide:

  • Fijar una semilla en las instancias necesarias para garantizar la reproducibilidad de resultados
  • Utilice TPESampler como método de muestreo
  • De XGBRegressor, optimice los siguientes hiperparámetros:
    • learning_rate buscando valores flotantes en el rango (0.001, 0.1)
    • n_estimators buscando valores enteros en el rango (50, 1000)
    • max_depth buscando valores enteros en el rango (3, 10)
    • max_leaves buscando valores enteros en el rango (0, 100)
    • min_child_weight buscando valores enteros en el rango (1, 5)
    • reg_alpha buscando valores flotantes en el rango (0, 1)
    • reg_lambda buscando valores flotantes en el rango (0, 1)
  • De OneHotEncoder, optimice el hiperparámetro min_frequency buscando el mejor valor flotante en el rango (0.0, 1.0)
  • Explique cada hiperparámetro y su rol en el modelo. ¿Hacen sentido los rangos de optimización indicados?
  • Fije el tiempo de entrenamiento a 5 minutos
  • Reportar el número de trials, el MAE y los mejores hiperparámetros encontrados. ¿Cómo cambian sus resultados con respecto a la sección anterior? ¿A qué se puede deber esto?
  • Guardar su modelo en un archivo .pkl

1.3.1 Elección de la mejor configuración¶

En las secciones analizamos tres modelos diferentes: DummyRegressor, XGBRegressor y XGBRegresor con restricción de monotonía. Como el modelo que obtuvo el menor valor de MAE para el conjunto de validación fue XGBRegressor sin restricción de monotonía, entonces utilizaremos ese modelo para esta sección.

1.3.2 Parámetros del modelo¶

In [33]:
XGBRegressor().get_params()
Out[33]:
{'objective': 'reg:squarederror',
 'base_score': None,
 'booster': None,
 'callbacks': None,
 'colsample_bylevel': None,
 'colsample_bynode': None,
 'colsample_bytree': None,
 'device': None,
 'early_stopping_rounds': None,
 'enable_categorical': False,
 'eval_metric': None,
 'feature_types': None,
 'gamma': None,
 'grow_policy': None,
 'importance_type': None,
 'interaction_constraints': None,
 'learning_rate': None,
 'max_bin': None,
 'max_cat_threshold': None,
 'max_cat_to_onehot': None,
 'max_delta_step': None,
 'max_depth': None,
 'max_leaves': None,
 'min_child_weight': None,
 'missing': nan,
 'monotone_constraints': None,
 'multi_strategy': None,
 'n_estimators': None,
 'n_jobs': None,
 'num_parallel_tree': None,
 'random_state': None,
 'reg_alpha': None,
 'reg_lambda': None,
 'sampling_method': None,
 'scale_pos_weight': None,
 'subsample': None,
 'tree_method': None,
 'validate_parameters': None,
 'verbosity': None}

1.3.3 Función objetivo¶

In [34]:
# Definimos la función objetivo para optuna
def objective_function(trial):
    # Definimos los hiperparámetros a optimizar para XGBRegressor
    xgb_params = {
        'learning_rate': trial.suggest_float('learning_rate', 0.001, 0.1),
        'n_estimators': trial.suggest_int('n_estimators', 50, 1000),
        'max_depth': trial.suggest_int('max_depth', 3, 10),
        'max_leaves': trial.suggest_int('max_leaves', 0, 100),
        'min_child_weight': trial.suggest_int('min_child_weight', 1, 5),
        'reg_alpha': trial.suggest_float('reg_alpha', 0, 1),
        'reg_lambda': trial.suggest_float('reg_lambda', 0, 1),
    }

    # Definimos los hiperparámetros a optimizar para OneHotEncoder
    ohe_params = {
        'min_frequency': trial.suggest_float('min_frequency', 0.0, 1.0),
    }

    # Definimos el preprocesador para las columnas
    preprocessor = ColumnTransformer(
        transformers = [
            ('num', MinMaxScaler(), numerical_features),
            ('cat', OneHotEncoder(sparse_output=False, min_frequency=ohe_params['min_frequency']), categorical_features)
        ],
        remainder = 'drop'
    )

    # Definimos la pipeline con XGBRegressor
    pipeline = Pipeline([
        ('date_transformer', date_transformer),
        ('preprocessor', preprocessor),
        ('regressor', XGBRegressor(seed=seed, **xgb_params)),
    ])
    
    # Entrenamos y predecimos
    pipeline.fit(X_train, y_train)
    y_pred = pipeline.predict(X_val)

    # Calculamos el MAE
    mae = mean_absolute_error(y_val, y_pred)

    return mae

1.3.4 Optimización¶

In [35]:
# Definimos el sampler
sampler = TPESampler(seed=seed)

# Definimos un study de optuna
optuna.logging.set_verbosity(optuna.logging.WARNING)
study = optuna.create_study(direction='minimize', sampler=sampler)

# Definimos el tiempo límite de ejecución (5 minutos)
timeout = 5 * 60

# Optimizamos
start_time = time.time()
study.optimize(objective_function, timeout=timeout, show_progress_bar=True)
end_time = time.time()
   0%|          | 00:00/05:00

1.3.5 Resultados¶

In [36]:
# Guardamos el mejor MAE
mae_xgb_optuna = study.best_value

# Entregamos los resultados
print(f"Number of trials: {len(study.trials)}")
print(f"Best trial MAE: {study.best_value:.4f}")
print(f"Best parameters: {study.best_params}")
print(f"Execution time: {end_time - start_time:.2f} seconds")
Number of trials: 180
Best trial MAE: 1932.1264
Best parameters: {'learning_rate': 0.07967157745047833, 'n_estimators': 966, 'max_depth': 8, 'max_leaves': 91, 'min_child_weight': 4, 'reg_alpha': 0.2597487635568112, 'reg_lambda': 0.4196469440202588, 'min_frequency': 0.058011986410824994}
Execution time: 301.50 seconds

1.3.6 Análisis de hiperparámetros y resultados¶

Explicaremos cada hiperparámetro del modelo XGBRegressor:

  • learning_rate: controla la contribución de cada árbol a la actualización del modelo para evitar sobreajuste.
  • n_estimators: indica la cantidad de árboles a construir, mientras más árboles mejor será el rendimiento del modelo, pero también será más caro computacionalmente.
  • max_depth: controla la profundidad máxima que puede tener cada árbol, mientras mayor sea la profundidad más fácil será para los árboles capturar relaciones más complejas, sin embargo se corre el riesgo de sobreajuste.
  • max_leaves: indica el máximo número de hojas que puede tener cada árbol, mientras mayor sea la cantidad de hojas límite, hay mayor facilidad a sobreajustarse.
  • min_child_weight: indica el peso mínimo necesario para separar una hoja, mientras mayor sea este valor más difícil será para el modelo sobreajustarse a los datos específicos.
  • reg_alpha: coeficiente de regularización L1.
  • reg_lambda: coeficiente de regularización L2. Estos dos últimos coeficientes ayudan a evitar el sobreajuste del modelo, ya sea penalizando las features más importantes (para alpha) y penalizando los pesos más grandes (para lambda).
  • min_frequency: indica la frecuencia mínima para que una categoría sea considerada frecuente.

Por como funciona cada hiperparámetro los rangos utilizados en la optimización son razonables.

Por otro lado, con respecto a los resultados de la optimización. El modelo realizó aproximadamente 200 trials y el mejor fue el trial 173 con un MAE de 1900. Recordando que el modelo anterior con los parámetros por defecto obtuvo un MAE de 2400, entonces podemos decir que la optimización de los hiperparámetros mejoró el desempeño del modelo. Esto se debe a que precisamente estamos utilizando una combinación de hiperparámetros que permite que el modelo capture bien el comportamiento de los datos.

1.3.7 Guardado del mejor modelo¶

In [37]:
# Obtenemos el mejor intento y sus hiperparametros
best_trial = study.best_trial
best_hyperparameters = best_trial.params

# Definimos el preprocesador para las columnas
preprocessor = ColumnTransformer(
    transformers = [
        ('num', MinMaxScaler(), numerical_features),
        ('cat', OneHotEncoder(sparse_output=False, min_frequency=best_hyperparameters['min_frequency']), categorical_features)
    ],
    remainder = 'drop'
)

# Definimos la pipeline con XGBRegressor
pipeline_xgb_optuna = Pipeline([
    ('date_transformer', date_transformer),
    ('preprocessor', preprocessor),
    ('regressor', XGBRegressor(seed=seed, **best_hyperparameters)),
])

# Entrenamos
pipeline_xgb_optuna.fit(X_train, y_train)

# Guardamos
pickle.dump(pipeline_xgb_optuna, open('models/xgb_regressor_optuna_model.pkl', 'wb'))
C:\Users\Daniel Minaya Vargas\anaconda3\lib\site-packages\xgboost\core.py:160: UserWarning: [19:54:21] WARNING: C:\buildkite-agent\builds\buildkite-windows-cpu-autoscaling-group-i-0750514818a16474a-1\xgboost\xgboost-ci-windows\src\learner.cc:742: 
Parameters: { "min_frequency" } are not used.

  warnings.warn(smsg, UserWarning)

1.4 Optimización de Hiperparámetros con Optuna y Prunners (1.7)¶

Después de optimizar el rendimiento de su modelo varias veces, Fiu le pregunta si no es posible optimizar el entrenamiento del modelo en sí mismo. Después de leer un par de post de personas de dudosa reputación en la deepweb, usted llega a la conclusión que puede cumplir este objetivo mediante la implementación de Prunning.

Vuelva a optimizar los mismos hiperparámetros que la sección pasada, pero esta vez utilizando Prunning en la optimización. En particular, usted debe:

  • Responder: ¿Qué es prunning? ¿De qué forma debería impactar en el entrenamiento?
  • Utilizar optuna.integration.XGBoostPruningCallback como método de Prunning
  • Fijar nuevamente el tiempo de entrenamiento a 5 minutos
  • Reportar el número de trials, el MAE y los mejores hiperparámetros encontrados. ¿Cómo cambian sus resultados con respecto a la sección anterior? ¿A qué se puede deber esto?
  • Guardar su modelo en un archivo .pkl

Nota: Si quieren silenciar los prints obtenidos en el prunning, pueden hacerlo mediante el siguiente comando:

optuna.logging.set_verbosity(optuna.logging.WARNING)

De implementar la opción anterior, pueden especificar show_progress_bar = True en el método optimize para más sabor.

Hint: Si quieren especificar parámetros del método .fit() del modelo a través del pipeline, pueden hacerlo por medio de la siguiente sintaxis: pipeline.fit(stepmodelo__parametro = valor)

Hint2: Este enlace les puede ser de ayuda en su implementación

1.4.1 Prunning¶

El pruning o poda es la interrupción prematura del entrenamiento de una configuración de hiperparámetros dada si se detecta que no está progresando adecuadamente. Este proceso se realiza mediante la monitorización de una métrica (observation_key) durante el entrenamiento y comparándola con un umbral. Si la métrica no mejora significativamente, el entrenamiento se detiene tempranamente, disminuyendo el tiempo que toma la optimización.

Esto ayuda a evitar sobreajuste de los modelos, pues si la métrica no cambia significativamente, entonces el modelo ya aprendió lo necesario y seguir iterando lo único que lograra es que el modelo memorice la data de entrenamiento para seguir mejorando la métrica de evaluación. De este modo, conseguimos un modelo menos propenso a sobreajuste y se optimizan los hiperparámetros de manera más eficiente.

1.4.2 Función objetivo¶

In [38]:
# Definimos la función objetivo para optuna
def objective_function_prunning(trial):
    # Definimos los hiperparámetros a optimizar para XGBRegressor
    xgb_params = {
        'learning_rate': trial.suggest_float('learning_rate', 0.001, 0.1),
        'n_estimators': trial.suggest_int('n_estimators', 50, 1000),
        'max_depth': trial.suggest_int('max_depth', 3, 10),
        'max_leaves': trial.suggest_int('max_leaves', 0, 100),
        'min_child_weight': trial.suggest_int('min_child_weight', 1, 5),
        'reg_alpha': trial.suggest_float('reg_alpha', 0, 1),
        'reg_lambda': trial.suggest_float('reg_lambda', 0, 1),
    }

    # Definimos los hiperparámetros a optimizar para OneHotEncoder
    ohe_params = {
        'min_frequency': trial.suggest_float('min_frequency', 0.0, 1.0),
    }

    # Definimos el preprocesador para las columnas
    preprocessor = ColumnTransformer(
        transformers = [
            ('num', MinMaxScaler(), numerical_features),
            ('cat', OneHotEncoder(sparse_output=False, min_frequency=ohe_params['min_frequency']), categorical_features)
        ],
        remainder = 'drop'
    )
    
    # Definimos el PrunningCallback
    pruninng_callback = optuna.integration.XGBoostPruningCallback(
        trial, observation_key='validation_1-rmse'
    )

    # Definimos la pipeline con XGBRegressor
    pipeline = Pipeline([
        ('date_transformer', date_transformer),
        ('preprocessor', preprocessor),
    ])
    
    # Aplicamos las transformaciones
    X_train_transformed = pipeline.named_steps['date_transformer'].transform(X_train)
    X_train_transformed = pipeline.named_steps['preprocessor'].fit_transform(X_train_transformed)

    X_val_transformed = pipeline.named_steps['date_transformer'].transform(X_val)
    X_val_transformed = pipeline.named_steps['preprocessor'].transform(X_val_transformed)

    # Entrenamos y predecimos con XGBRegressor
    xgb_regressor = XGBRegressor(seed=seed, **xgb_params)
    xgb_regressor.set_params(callbacks=[pruninng_callback])
    xgb_regressor.fit(X_train_transformed, y_train,
                      eval_set=[(X_train_transformed, y_train), (X_val_transformed, y_val)],
                      verbose=False
                     )
    y_pred = xgb_regressor.predict(X_val_transformed)

    # Calculamos el MAE
    mae = mean_absolute_error(y_val, y_pred)

    return mae

1.4.3 Optimización¶

In [39]:
# Definimos el sampler
sampler = TPESampler(seed=seed)

# Definimos un study de optuna
optuna.logging.set_verbosity(optuna.logging.WARNING)
prunning_study = optuna.create_study(direction='minimize', sampler=sampler)

# Definimos el tiempo límite de ejecución (5 minutos)
timeout = 5 * 60

# Optimizamos
start_time = time.time()
prunning_study.optimize(objective_function_prunning, 
                        timeout=timeout, 
                        show_progress_bar=True,
                       )
end_time = time.time()
   0%|          | 00:00/05:00

1.4.4 Análisis de resultados¶

In [40]:
# Guardamos el mejor MAE
mae_xgb_optuna_prunning = prunning_study.best_value

# Entregamos los resultados sin Prunning
print("SIN PRUNNING")
print(f"Number of trials: {len(study.trials)}")
print(f"Best trial MAE: {study.best_value:.4f}")
print(f"Best parameters: {study.best_params}")
print(f"Execution time: {end_time - start_time:.2f} seconds")
print("\n")

# Entregamos los resultados con Prunning
print("CON PRUNNING")
print(f"Number of trials: {len(prunning_study.trials)}")
print(f"Best trial MAE: {prunning_study.best_value:.4f}")
print(f"Best parameters: {prunning_study.best_params}")
print(f"Execution time: {end_time - start_time:.2f} seconds")
SIN PRUNNING
Number of trials: 180
Best trial MAE: 1932.1264
Best parameters: {'learning_rate': 0.07967157745047833, 'n_estimators': 966, 'max_depth': 8, 'max_leaves': 91, 'min_child_weight': 4, 'reg_alpha': 0.2597487635568112, 'reg_lambda': 0.4196469440202588, 'min_frequency': 0.058011986410824994}
Execution time: 300.04 seconds


CON PRUNNING
Number of trials: 335
Best trial MAE: 1935.7160
Best parameters: {'learning_rate': 0.09658995703599647, 'n_estimators': 952, 'max_depth': 7, 'max_leaves': 77, 'min_child_weight': 3, 'reg_alpha': 0.771645747151882, 'reg_lambda': 0.733338284255412, 'min_frequency': 0.022818610800548842}
Execution time: 300.04 seconds

Podemos ver que el MAE no cambio significativamente, con respecto a la optimización sin prunning, ambos nos entregaron modelos con un MAE de alrededor de 1900, sin embargo, lo interesante es ver cómo cambiaron los hiperparámetros óptimos. Podemos ver que n_estimators, max_depth y max_leaves disminuyeron al aplicar prunning, lo que indica que el modelo está considerando árboles más simples y además vemos también que los coeficientes de regularización aumentan, por lo que el modelo resultante de estos hiperparámetros será más robusto y tendrá bajas posibilidades de estar sobreajustado.

1.4.5 Guardado del mejor modelo¶

In [41]:
# Obtenemos el mejor intento y sus hiperparametros
best_trial = prunning_study.best_trial
best_hyperparameters = best_trial.params

# Definimos el preprocesador para las columnas
preprocessor = ColumnTransformer(
    transformers = [
        ('num', MinMaxScaler(), numerical_features),
        ('cat', OneHotEncoder(sparse_output=False, min_frequency=best_hyperparameters['min_frequency']), categorical_features)
    ],
    remainder = 'drop'
)

# Definimos la pipeline con XGBRegressor
pipeline_xgb_optuna_prunning = Pipeline([
    ('date_transformer', date_transformer),
    ('preprocessor', preprocessor),
    ('regressor', XGBRegressor(seed=seed, **best_hyperparameters)),
])

# Entrenamos
pipeline_xgb_optuna_prunning.fit(X_train, y_train)

# Guardamos
pickle.dump(pipeline_xgb_optuna_prunning, open('models/xgb_regressor_optuna_prunning_model.pkl', 'wb'))
C:\Users\Daniel Minaya Vargas\anaconda3\lib\site-packages\xgboost\core.py:160: UserWarning: [19:59:23] WARNING: C:\buildkite-agent\builds\buildkite-windows-cpu-autoscaling-group-i-0750514818a16474a-1\xgboost\xgboost-ci-windows\src\learner.cc:742: 
Parameters: { "min_frequency" } are not used.

  warnings.warn(smsg, UserWarning)

1.5 Visualizaciones (0.5 puntos)¶

Satisfecho con su trabajo, Fiu le pregunta si es posible generar visualizaciones que permitan entender el entrenamiento de su modelo.

A partir del siguiente enlace, genere las siguientes visualizaciones:

  • Gráfico de historial de optimización
  • Gráfico de coordenadas paralelas
  • Gráfico de importancia de hiperparámetros

Comente sus resultados: ¿Desde qué trial se empiezan a observar mejoras notables en sus resultados? ¿Qué tendencias puede observar a partir del gráfico de coordenadas paralelas? ¿Cuáles son los hiperparámetros con mayor importancia para la optimización de su modelo?

1.5.1 Modelo XGBRegressor sin prunning¶

In [42]:
plot_optimization_history(study)
In [43]:
plot_parallel_coordinate(study)
In [44]:
plot_param_importances(study)

1.5.2 Modelo XGBRegressor con prunning¶

In [45]:
plot_optimization_history(prunning_study)
In [46]:
plot_parallel_coordinate(prunning_study)
In [47]:
plot_param_importances(prunning_study)

1.5.3 Análisis de los gráficos¶

Podemos ver que para el trial 25 la función objetivo deja de disminuir su valor, por lo que podemos decir que hasta este trial los modelos dejan de mejorar. Esto ocurre independiente de si usamos prunning o no, sin embargo, podemos notar que cuando utilizamos prunning se ignoran los trials que se alejan del posible mínimo encontrado por la función objetivo, por lo que el gráfico se ve menos ruidoso al ignorar valores innecesarios.

Pasando al gráfico de coordenadas paralelas, podemos ver que los hiperparámetros que alcanzan los valores óptimos tienden a tener valores para learning_rate y n_estimatores altos, y valores para min_frequency bajos, pasando por valores medios para max_depth, max_leaves, min_child_weight. Sin embargo, la principal diferencia que podemos notar al usar prunning es en los coeficientes de regularización, donde con prunning se toman valores más altos, provocando que el modelo resultante sea más robusto.

Pasando al gráfico de importancias, vemos que en ambas optimizaciones el hiperparámetro min_frequency es el más importante, de hecho, es 0.95 para la optimización sin prunning, lo que indicaría que es más importante la codificación de las variables categóricas que el ajuste de hiperparámetros del modelo, sin embargo, al agregar prunning podemos observar que la optimización le da importancia a otros parámetros propios del modelo como el learning_rate.

1.6 Síntesis de resultados (0.3)¶

Finalmente, genere una tabla resumen del MAE obtenido en los 5 modelos entrenados (desde Baseline hasta XGBoost con Constraints, Optuna y Prunning) y compare sus resultados. ¿Qué modelo obtiene el mejor rendimiento?

Por último, cargue el mejor modelo, prediga sobre el conjunto de test y reporte su MAE. ¿Existen diferencias con respecto a las métricas obtenidas en el conjunto de validación? ¿Porqué puede ocurrir esto?

1.6.1 Tabla resumen¶

In [48]:
# Agrupamos los modelos con su MAE en validación
data = {
    'Model': ['Dummy', 'XGB', 'XGBR+Monotonic', 'XGB+Optuna', 'XGB+Prunning'],
    'MAE': [mae_dummy, mae_xgb, mae_xgb_monotonic, mae_xgb_optuna, mae_xgb_optuna_prunning]
}

# Pasamos a DataFrame y mostramos
summary = pd.DataFrame(data)
display(summary)
Model MAE
0 Dummy 13308.134751
1 XGB 2441.852488
2 XGBR+Monotonic 2477.778417
3 XGB+Optuna 1932.126397
4 XGB+Prunning 1935.715991

1.6.2 Modelo con mejor rendimiento¶

El modelo con mejor rendimiento, es decir, con menor valor de MAE en el conjunto de validación fue el XGBRegressor con los hiperparámetros optimizados con optuna sin prunning, el cual alcanzó un MAE de 1932,12.

1.6.3 Predicción en conjunto de prueba¶

In [49]:
# Cargamos el modelo con menor MAE (XGB+Optuna)
best_model_path = 'models/xgb_regressor_optuna_model.pkl'
with open(best_model_path, 'rb') as file:
    loaded_model = pickle.load(file)

# Predecimos en el conjunto de prueba
y_pred = loaded_model.predict(X_test)

# Calculamos el MAE en el conjunto de prueba
mae_test = mean_absolute_error(y_test, y_pred)

print(f"MAE en el conjunto de prueba: {mae_test}")
MAE en el conjunto de prueba: 1990.9377157291315

Podemos ver que el MAE obtenido en el conjunto de prueba es 1990,93, el cual es levemente mayor al MAE obtenido en el conjunto de validación, sin embargo, esto es normal, ya que para optimizar los hiperparámetros del modelo se utilizo este conjunto como referencia, es decir, los hiperparámetros elegidos logran tener el menor MAE para el conjunto de validación. Igual es relevante notar que la diferencia en el MAE no es muy grande, por lo que podemos concluir que nuestro modelo generaliza bien y es un buen regresor.

Conclusión¶

Eso ha sido todo para el lab de hoy, recuerden que el laboratorio tiene un plazo de entrega de una semana. Cualquier duda del laboratorio, no duden en contactarnos por mail o U-cursos.